﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using System.Windows.Media.Animation;
using System;
using System.Windows.Media;
using Xceed.Silverlight.Compatibility;

namespace Xceed.Silverlight.DragDrop
{
  internal sealed class DragDropAdornerManager : DependencyObject
  {
    #region Singleton Implementation

    private static DragDropAdornerManager InstanceCache;

    public static DragDropAdornerManager Instance
    {
      get
      {
        if( DragDropAdornerManager.InstanceCache == null )
        {
          DragDropAdornerManager.InstanceCache = new DragDropAdornerManager();
        }

        return DragDropAdornerManager.InstanceCache;
      }
    }

    #endregion

    #region Constructors

    private DragDropAdornerManager()
    {
      this.IsDraggedElementConfigurationPreservedCore = false;
    }

    #endregion

    #region DraggedElementImageSource Attached Property

    // Attached Property used by the Default
    internal static readonly DependencyProperty DraggedElementImageSourceProperty = DependencyProperty.RegisterAttached(
      "DraggedElementImageSource",
      typeof( ImageSource ),
      typeof( DragDropAdornerManager ),
      new PropertyMetadata( null ) );

    public static ImageSource GetDraggedElementImageSource( DependencyObject obj )
    {
      return ( ImageSource )obj.GetValue( DragDropAdornerManager.DraggedElementImageSourceProperty );
    }

    internal static void SetDraggedElementImageSource( DependencyObject obj, ImageSource value )
    {
      obj.SetValue( DragDropAdornerManager.DraggedElementImageSourceProperty, value );
    }

    #endregion

    #region ContentPresenterProvider Attached Property

    internal static readonly DependencyProperty ContentPresenterProviderProperty = DependencyProperty.RegisterAttached(
      "ContentPresenterProvider",
      typeof( IProvideDraggedContentPresenter ),
      typeof( DragDropAdornerManager ),
      new PropertyMetadata( ( IProvideDraggedContentPresenter )null ) );

    public static IProvideDraggedContentPresenter GetContentPresenterProvider( DependencyObject obj )
    {
      return ( IProvideDraggedContentPresenter )obj.GetValue( DragDropAdornerManager.ContentPresenterProviderProperty );
    }

    internal static void SetContentPresenterProvider( DependencyObject obj, IProvideDraggedContentPresenter value )
    {
      obj.SetValue( DragDropAdornerManager.ContentPresenterProviderProperty, value );
    }

    #endregion

    #region IsDraggedElementConfigurationPreserved Property

    internal static bool IsDraggedElementConfigurationPreserved
    {
      get
      {
        return DragDropAdornerManager.Instance.IsDraggedElementConfigurationPreservedCore;
      }
      set 
      {
        DragDropAdornerManager.Instance.IsDraggedElementConfigurationPreservedCore = value;
      }
    }

    private bool IsDraggedElementConfigurationPreservedCore { get; set; }

    #endregion

    public static void PrepareAdorner( DragDropContext dragContext,
      MouseEventArgs e )
    {
      DragDropAdornerManager.Instance.PrepareAdornerWorker( dragContext, e );
    }

    private void PrepareAdornerWorker( DragDropContext dragContext, MouseEventArgs e )
    {
      this.CreateDragDropContainer();

      m_dragContext = dragContext;
      m_draggedElement = dragContext.DraggedElement as FrameworkElement;
      Debug.Assert( m_draggedElement != null );

      if( m_draggedElement == null )
        return;

      UIElement draggedElementRoot = GetElementRoot( dragContext, m_draggedElement );

      m_draggedElement.CaptureMouse();

      DraggedElementConfiguration configuration = dragContext.Configuration;
      bool positionAdornerRelativeToAdornedElement = true;

      if( configuration != null )
      {
        positionAdornerRelativeToAdornedElement = configuration.PositionAdornerRelativeToElement;
      }
      else
      {
        positionAdornerRelativeToAdornedElement = true;
      }

      // Get the position relative to the dragged element to be able to 
      // offset the dragged elements at the actual mouse position
      Point positionToDraggedElement = ( !positionAdornerRelativeToAdornedElement )
        ? e.GetPosition( m_draggedElement )
        : new Point();

#if SILVERLIGHT
      m_draggedPopup.HorizontalOffset = e.GetPosition( draggedElementRoot ).X - e.GetPosition( m_draggedElement ).X;
      m_draggedPopup.VerticalOffset = e.GetPosition( draggedElementRoot ).Y - e.GetPosition( m_draggedElement ).Y;
#else
      m_draggedWindow.Left = 0;
      m_draggedWindow.Top = 0;
#endif

      m_LastMousePosition = e.GetPosition( null );

      // Ensure to stop the fade out animation and
      // to clean the DraggedPopup if a drag operation
      // was started before the fade out storyboard was
      // completed
      if( this.GetDragDropContainerContent() != null )
      {
        if( m_fadeOutDraggedAdornersStoryboard != null )
          m_fadeOutDraggedAdornersStoryboard.Stop();

        this.ClearDraggedPopup();
      }

      // Create a new Canvas in the Popup/Window since the 
      // Child is set to null when Drag is completed/aborted
      DragDropCanvas canvas = new DragDropCanvas();
      this.AddToDragDropContainer( canvas );

      ContentPresenter draggedContentPresenter = this.CreateContentPresenterForDraggedElement( m_draggedElement,
        positionToDraggedElement,
        dragContext );

      canvas.Children.Add( draggedContentPresenter );

      IEnumerable<FrameworkElement> siblingDraggedElements = dragContext.SiblingDraggedElements;

      if( ( siblingDraggedElements != null ) && ( configuration.DisplaySiblingElements ) )
      {
        foreach( FrameworkElement siblingDraggedElement in siblingDraggedElements )
        {
          ContentPresenter siblingAdorner = this.CreateContentPresenterForDraggedElement( siblingDraggedElement,
          positionToDraggedElement,
          dragContext );

          canvas.Children.Add( siblingAdorner );
        }
      }

      this.FadeInDraggedAdorners( configuration );

      this.ShowDragDropContainer( true );
    }

    private ContentPresenter CreateContentPresenterForDraggedElement( FrameworkElement draggedElement,
      Point mousePositionToDraggedElement,
      DragDropContext dragContext )
    {
      DraggedElementConfiguration configuration = dragContext.Configuration;
      bool isDraggedElement = ( m_draggedElement == draggedElement );

      ContentPresenter contentPresenter = null;
      IProvideDraggedContentPresenter contentPresenterProvider = draggedElement as IProvideDraggedContentPresenter;

      if( contentPresenterProvider != null )
      {
        contentPresenter = contentPresenterProvider.CreateDraggedContentPresenter();
        contentPresenterProvider.PrepareDraggedContentPresenter( contentPresenter );
        DragDropAdornerManager.SetContentPresenterProvider( contentPresenter, contentPresenterProvider );
      }
      else
      {
        contentPresenter = new ContentPresenter();
      }

      UIElement draggedElementRoot = GetElementRoot( dragContext, m_draggedElement );

#if SILVERLIGHT
      Point draggedElementToMainDraggedElement = draggedElement.TransformToVisual( m_draggedElement ).Transform( new Point() );

#if WINDOWS_PHONE
#else
      draggedElementToMainDraggedElement.X /= Application.Current.Host.Content.ZoomFactor;
      draggedElementToMainDraggedElement.Y /= Application.Current.Host.Content.ZoomFactor;
#endif
#endif

      Point contentPresenterPositionBias = mousePositionToDraggedElement;

      if( dragContext.Configuration != null )
      {
        if( !dragContext.Configuration.ApplyDraggedElementRenderTransform )
        {
#if SILVERLIGHT
          contentPresenter.RenderTransform = new TransformGroup();
#else
          // Remove scaling transforms
          contentPresenter.RenderTransform = this.ExtractNonScalingTransform( contentPresenter.RenderTransform );

          // Get draggedElement's Scale Factor
          double xScaleFactor = 1.0d;
          double yScaleFactor = 1.0d;
          TransformGroup transformGroup = draggedElement.RenderTransform as TransformGroup;
          if( transformGroup != null )
          {
            foreach( Transform child in transformGroup.Children )
            {
              ScaleTransform scaleTransform = child as ScaleTransform;
              {
                if( scaleTransform != null )
                {
                  xScaleFactor = scaleTransform.ScaleX;
                  yScaleFactor = scaleTransform.ScaleY;
                }
              }
            }
          }

          contentPresenterPositionBias = new Point( ( mousePositionToDraggedElement.X * xScaleFactor ), ( mousePositionToDraggedElement.Y * yScaleFactor ) );
#endif
        }
      }

#if SILVERLIGHT
      Canvas.SetLeft( contentPresenter, draggedElementToMainDraggedElement.X + contentPresenterPositionBias.X );
      Canvas.SetTop( contentPresenter, draggedElementToMainDraggedElement.Y + contentPresenterPositionBias.Y );
#else
      Point draggedElementToDesktop = draggedElement.PointToScreen( new Point( 0, 0 ) );

      Canvas.SetLeft( contentPresenter, draggedElementToDesktop.X + contentPresenterPositionBias.X );
      Canvas.SetTop( contentPresenter, draggedElementToDesktop.Y + contentPresenterPositionBias.Y );
#endif

      contentPresenter.IsHitTestVisible = false;

      if( configuration != null )
      {
        Binding binding = new Binding();
        binding.Mode = BindingMode.OneWay;
        binding.Source = configuration;
        binding.Path = ( isDraggedElement )
          ? new PropertyPath( "ElementTemplate" )
          : new PropertyPath( "SiblingElementTemplate" );

        contentPresenter.SetBinding( ContentPresenter.ContentTemplateProperty, binding );
      }

      if( dragContext.Data != null )
      {
        contentPresenter.Content = dragContext.Data;
      }
      // We can't assign the same DataContext if it is a UIElement
      // to avoid any exceptions of kind "Already the child of another
      // UIElement"
      else if( ( draggedElement.DataContext as UIElement ) == null )
      {
        contentPresenter.Content = draggedElement.DataContext;
      }

      // Set the DragContext on the adorner to be able to bind
      // to the any of its property
      DragDropContext.SetParentDragContext( contentPresenter, dragContext );

      // Set the DraggedElementAdorner AttachedProperty on the ContentPresenter
      // to be able to display the copy of the dragged element in an Image Control
      // if no ContentTemplate is explicitly defined on the DraggedElementConfiguration
      DragDropAdornerManager.SetDraggedElementImageSource( contentPresenter, CompatibilityUtils.CreateImageFrom( draggedElement ) );

      return contentPresenter;
    }

#if !SILVERLIGHT
    private Transform ExtractNonScalingTransform( Transform transform )
    {
      // If Transform is scaling, cancel it
      if( transform is ScaleTransform )
      {
        return null;
      }

      // If Transform is TransformGroup, remove scaling
      TransformGroup group = transform as TransformGroup;
      if( group != null )
      {
        TransformGroup result = new TransformGroup();

        foreach( Transform child in group.Children )
        {
          Transform newChild = ExtractNonScalingTransform( child );

          if( newChild != null )
          {
            result.Children.Add( newChild );
          }
        }

        return result;
      }

      // Otherwise, keep the transform as is
      return transform;
    }
#endif

    public static void OnAdornedElementMouseMove( MouseEventArgs e )
    {
      DragDropAdornerManager.Instance.OnAdornedElementMouseMoveWorker( e );
    }

    private void OnAdornedElementMouseMoveWorker( MouseEventArgs e )
    {
      Point actualMousePosition = e.GetPosition( null );

      double distX = actualMousePosition.X - m_LastMousePosition.X;
      double distY = actualMousePosition.Y - m_LastMousePosition.Y;

      this.MoveDragDropContainer( distX, distY );

      m_LastMousePosition = actualMousePosition;
    }

    public static void HideAdorner()
    {
      DragDropAdornerManager.Instance.HideAdornerWorker();
    }

    private void HideAdornerWorker()
    {
      if( m_draggedElement != null )
      {
        m_draggedElement.ReleaseMouseCapture();
        m_draggedElement = null;
      }

      if( this.GetDragDropContainer() != null )
      {
        if( m_dragContext != null )
        {
          // Try to start the storyboard to fade out every adorners
          // if it is not possible, clean the DraggedPopup immediately
          if( this.FadeOutDraggedAdorners( m_dragContext.Configuration ) )
            return;
        }

        this.ClearDraggedPopup();
      }
    }

    private void FadeInDraggedAdorners( DraggedElementConfiguration configuration )
    {
      if( configuration == null )
        return;

      if( this.GetDragDropContainer() == null )
        return;

      DragDropCanvas canvas = this.GetDragDropContainerContent() as DragDropCanvas;

      if( canvas == null )
        return;

      Storyboard fadeInDraggedAdornersStoryboard = new Storyboard();

      foreach( FrameworkElement child in canvas.Children )
      {
        DoubleAnimation opacityAnimation = new DoubleAnimation();
        opacityAnimation.From = 0d;
        opacityAnimation.To = 1d;
        opacityAnimation.Duration = configuration.GetDraggedElementFadeInDuration();

        Storyboard.SetTarget( opacityAnimation, child );
        Storyboard.SetTargetProperty( opacityAnimation, new PropertyPath( "Opacity" ) );
        fadeInDraggedAdornersStoryboard.Children.Add( opacityAnimation );
      }

      fadeInDraggedAdornersStoryboard.Begin();
    }

    private bool FadeOutDraggedAdorners( DraggedElementConfiguration configuration )
    {
      if( configuration == null )
        return false;

      if( this.GetDragDropContainer() == null )
        return false;

      DragDropCanvas canvas = this.GetDragDropContainerContent() as DragDropCanvas;

      if( canvas == null )
        return false;

      if( m_fadeOutDraggedAdornersStoryboard != null )
        m_fadeOutDraggedAdornersStoryboard.Stop();

      m_fadeOutDraggedAdornersStoryboard = new Storyboard();

      foreach( UIElement child in canvas.Children )
      {
        DoubleAnimation opacityAnimation = new DoubleAnimation();
        opacityAnimation.To = 0d;
        opacityAnimation.Duration = m_dragContext.Configuration.GetDraggedElementFadeOutDuration();

        Storyboard.SetTarget( opacityAnimation, child );
        Storyboard.SetTargetProperty( opacityAnimation, new PropertyPath( "Opacity" ) );
        m_fadeOutDraggedAdornersStoryboard.Children.Add( opacityAnimation );
      }

      m_fadeOutDraggedAdornersStoryboard.Completed += this.OnFadeOutDraggedAdornersStoryboard_Completed;
      m_fadeOutDraggedAdornersStoryboard.Begin();
      return true;
    }

    private void OnFadeOutDraggedAdornersStoryboard_Completed( object sender, EventArgs e )
    {
      Storyboard storyboard = sender as Storyboard;

      if( storyboard != null )
        storyboard.Completed -= this.OnFadeOutDraggedAdornersStoryboard_Completed;

      this.ClearDraggedPopup();
    }

    private void ClearDraggedPopup()
    {
      if( this.GetDragDropContainer() == null )
        return;

      this.ShowDragDropContainer( false );

      DragDropCanvas canvas = this.GetDragDropContainerContent() as DragDropCanvas;

      if( canvas == null )
        return;

      foreach( ContentPresenter child in canvas.Children )
      {
        IProvideDraggedContentPresenter provideDraggedContentPresenter = child.GetValue( DragDropAdornerManager.ContentPresenterProviderProperty ) as IProvideDraggedContentPresenter;

        child.ClearValue( DragDropAdornerManager.ContentPresenterProviderProperty );
        child.ClearValue( DragDropContext.ParentDragContextProperty );
        child.ClearValue( FrameworkElement.DataContextProperty );
        child.ClearValue( DragDropAdornerManager.DraggedElementImageSourceProperty );
        child.ClearValue( ContentPresenter.ContentTemplateProperty );

        if( provideDraggedContentPresenter != null )
          provideDraggedContentPresenter.ClearDraggedContentPresenter( child );
      }

      canvas.Children.Clear();

      this.ClearDragDropContainer();

      if( !this.IsDraggedElementConfigurationPreservedCore )
      {
        m_dragContext.Configuration.ElementTemplate = null;
      }

      m_dragContext = null;
    }

    private UIElement GetElementRoot( DragDropContext dragContext, UIElement draggedElement )
    {
      UIElement draggedElementRoot = dragContext.DraggedElementRoot;
      if( draggedElementRoot == null )
      {
#if SILVERLIGHT
        draggedElementRoot = Application.Current.RootVisual;
#else
        draggedElementRoot = ( UIElement )VisualTreeHelper.GetParent( draggedElement );
        while( ( draggedElementRoot != null ) && !( draggedElementRoot is Window ) )
        {
          draggedElementRoot = ( UIElement )VisualTreeHelper.GetParent( draggedElementRoot );
        }
#endif
      }

      return draggedElementRoot;
    }

    private UIElement GetDragDropContainer()
    {
#if SILVERLIGHT
      return m_draggedPopup;
#else
      return m_draggedWindow;
#endif
    }

    private object GetDragDropContainerContent()
    {
#if SILVERLIGHT
      return m_draggedPopup.Child;
#else
      return m_draggedWindow.Content;
#endif
    }

    private void ShowDragDropContainer( bool isShowing )
    {
#if SILVERLIGHT
      m_draggedPopup.IsOpen = isShowing;
#else
      if( isShowing )
        m_draggedWindow.Show();
      else
        m_draggedWindow.Hide();
#endif
    }

    private void CreateDragDropContainer()
    {
#if SILVERLIGHT
      if( m_draggedPopup == null )
      {
        m_draggedPopup = new Popup();
        m_draggedPopup.IsHitTestVisible = false;
      }
#else
      if( m_draggedWindow == null )
      {
        m_draggedWindow = new Window();
        m_draggedWindow.WindowStyle = WindowStyle.None;
        m_draggedWindow.AllowsTransparency = true; 
        m_draggedWindow.Background = null;
        m_draggedWindow.SizeToContent = SizeToContent.WidthAndHeight;
        m_draggedWindow.ShowInTaskbar = false;
      }
#endif
    }

    private void ClearDragDropContainer()
    {
#if SILVERLIGHT
      m_draggedPopup.Child = null;
      m_draggedPopup.DataContext = null;
#else
      m_draggedWindow.Content = null;
      m_draggedWindow.DataContext = null;
      m_draggedWindow.Close();
      m_draggedWindow = null;
#endif
    }

    private void AddToDragDropContainer( DragDropCanvas canvas )
    {
#if SILVERLIGHT
      m_draggedPopup.Child = canvas;
#else
      m_draggedWindow.Content = canvas;
#endif
    }

    private void MoveDragDropContainer( double distX, double distY )
    {
#if SILVERLIGHT
      m_draggedPopup.HorizontalOffset = m_draggedPopup.HorizontalOffset + distX;
      m_draggedPopup.VerticalOffset = m_draggedPopup.VerticalOffset + distY;
#else
      if( m_draggedWindow != null )
      {
        m_draggedWindow.Left = m_draggedWindow.Left + distX;
        m_draggedWindow.Top = m_draggedWindow.Top + distY;
      }
#endif
    }

    #region Private Fields

    private Storyboard m_fadeOutDraggedAdornersStoryboard;
    private DragDropContext m_dragContext;
#if SILVERLIGHT
    private Popup m_draggedPopup;
#else
    private Window m_draggedWindow;
#endif
    private FrameworkElement m_draggedElement;
    private Point m_LastMousePosition;

    #endregion
  }
}
